{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 一、蓝牙的前世今生(1.0 到 5.0)：\n",
    "# 1. 蓝牙的简介:\n",
    "从1998年诞生到现在, 不知不觉这个世界将有[40亿蓝牙设备](https://www.shenzhenware.com/articles/12386)了. 蓝牙经历了1.0到5.0的技术变迁, 从音频传输、图文传输、视频传输, 再到以低功耗为主打的物联网传输,渗透到方方面面. \n",
    "# 2. \"蓝牙(bluetooth)\"的由来:\n",
    "蓝牙(bluetooth)一词取自于十世纪丹麦国王哈拉尔(Harald Bluetooth)的名字.\n",
    "![bluetooth](./images/bluetooth.jpg)\n",
    "而将\"蓝牙\"与后来无线通讯技术标准关联到一起的,是来自英特尔的工程师Jim Kardach. 他在一次无线通讯行业会议上, 提议将「Bluetooth」作为无线通讯技术标准的名称.\n",
    "**历史小故事**:\n",
    "由于哈拉尔国王以统一了因宗教战争和领土争议而分裂的挪威与丹麦而闻名于世, 国王的成就与 Jim Kardach 的理念不谋而合, 他希望蓝牙也可以成为统一的通用传输标准——将所有分散的设备与内容互联互通. 蓝牙的 LOGO 来自后弗萨克文的符文组合, 将哈拉尔国王名字的首字母 H 和 B 拼在一起, 成为了今天大家熟知的蓝色徽标.\n",
    "![hbh](./images/hb.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. 蓝牙的起源 & 发展:\n",
    "### 2.1 蓝牙的起源:\n",
    "蓝牙的历史实际要追溯到二战时期. 蓝牙的核心是短距离无线点通讯, 它的基础来自于[跳频扩频(FHSS)技术](https://baike.baidu.com/item/%E8%B7%B3%E9%A2%91%E6%8A%80%E6%9C%AF)\n",
    "蓝牙技术开始于爱立信在1994年创制的方案, 该方案旨在研究移动电话和其他配件间进行低功耗,低成本无线通信连接的方法.发明者希望为设备间的无线通讯创造一组统一规则(标准协议),以解决用户间互不兼容的移动电子设备的通讯问题, 用于代替[RS-232串口通讯协议](https://zh.wikipedia.org/zh-hans/RS-232).\n",
    "\n",
    "\n",
    "1998年5月20日, 爱立信联合IBM、英特尔、诺基亚、东芝等5家厂商成立[特别兴趣小组(Special Interest Group, SIG)](https://www.bluetooth.com/about-us/our-history/), 即:蓝牙技术联盟的前身, 目标是开发一个低成本、高效益、可以在短距离范围内随意无线连接的蓝牙技术标准. 同年蓝牙退出0.7规格,支持Baseband 与 LMP(Link Manager Protocol)通讯协定两部分.\n",
    "\n",
    "\n",
    "1999年,先后推出0.8 版、0.9 版、1.0 Draft 版. 完成了 SDP(Service Discovery Protocol)协定和 TCS(Telephony Control Specification)协定.\n",
    "\n",
    "\n",
    "1999 年 7 月 26 日正式公布 1.0A 版, 确定使用 2.4GHz 频段. 和当时流行的红外线技术相比, 蓝牙有着更高的传输速度, 而且不需要像红外线那样进行接口对接口的连接, 所有蓝牙设备基本上只要在有效通讯范围内使用, 就可以进行随时连接\n",
    "\n",
    "\n",
    "1999 年下半年, 微软、摩托罗拉、三星、朗讯与蓝牙特别小组的五家公司共同发起成立了蓝牙技术推广组织\n",
    "\n",
    "![bth](./images/bth.jpg)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 第一代蓝牙: 关于短距离通讯早期的探索:\n",
    "#### 1999年: 蓝牙1.0\n",
    "早期的蓝牙1.0A和1.0B版, 确定使用2.4GHz频谱. 同时, 在两个设备链接(Handshaking)的过程中, 蓝牙硬件的地址(BD_ADDR)会被发送出去, 在协议的层面上不能做到匿名, 造成泄露数据的危险.因此, 当 1.0 版本推出以后, 蓝牙并未立即受到广泛的应用. 除了当时对应蓝牙功能的电子设备种类少, 蓝牙装置也十分昂贵.\n",
    "#### 2001年: 蓝牙1.1\n",
    "蓝牙1.1版正式列入IEEE 802.15.1标准, 该标准定义了物理层(PHY)和媒体访问控制(MAC)规范, 用于设备间的无线连接, 传输率为 0.7Mbps. 但因为是早期设计, 容易受到同频率之间产品干扰, 影响通讯质量.\n",
    "#### 2003年: 蓝牙1.2\n",
    "蓝牙1.2针对1.0版暴露出的安全行问题, 完善了匿名方式,; 新增了屏蔽设备的硬件地址(BD_ADDR)功能, 保护用户免受身份嗅探攻击和跟踪, 同时向下兼容1.1;  新增了四项新功能:\n",
    "- AFH(Adaptive Frequency Hopping)适应性跳频技术, 减少了蓝牙产品与其它无线通讯装置之间所产生的干扰问题;\n",
    "- eSCO(Extended Synchronous Connection-Oriented links)延伸同步连结导向信道技术, 用于提供 QoS 的音频传输, 进一步满足高阶语音与音频产品的需求；\n",
    "- Faster Connection 快速连接功能, 可以缩短重新搜索与再连接的时间, 使连接过程更为稳定快速;\n",
    "- 支持 Stereo 音效的传输要求, 但只能以单工方式工作"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 第二代蓝牙：发力传输速率的 EDR 时代:\n",
    "#### 2004年: 蓝牙2.0\n",
    "新增EDR(Enhanced Data Rate)技术通过提高多任务处理和多种蓝牙设备同时运行的能力, 提高设备传输率可达3Mbps;蓝牙2.0支持双工模式;EDR 技术通过减少工作负债循环来降低功耗, 由于带宽的增加, 蓝牙 2.0 增加了连接设备的数量.\n",
    "\n",
    "#### 2007年: 蓝牙2.1\n",
    "蓝牙 2.1 新增了 Sniff Subrating 省电功能, 将设备间相互确认的讯号发送时间间隔从旧版的 0.1 秒延长到 0.5 秒左右, 从而让蓝牙芯片的工作负载大幅降低;另外, 新增 SSP 简易安全配对功能, 改善了蓝牙设备的配对体验, 同时提升了使用和安全强度;支持 NFC 近场通信, 只要将两个内置有 NFC 芯片的蓝牙设备相互靠近, 配对密码将通过 NFC 进行传输, 无需手动输入."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 第三代蓝牙：High Speed, 传输速率高达 24Mbps\n",
    "#### 2009 年：蓝牙 3.0\n",
    "蓝牙 3.0 新增了可选技术 High Speed, High Speed 可以使蓝牙调用 802.11 WiFi 用于实现高速数据传输, 传输率高达 24Mbps, 是蓝牙 2.0 的 8 倍, 轻松实现录像机至高清电视、PC 至 PMP、UMPC 至打印机之间的资料传输\n",
    "\n",
    "蓝牙 3.0 的核心是 AMP(Generic Alternate MAC/PHY), 这是一种全新的交替射频技术, 允许蓝牙协议栈针对任一任务动态地选择正确射频\n",
    "\n",
    "功耗方面, 蓝牙 3.0 引入了 EPC 增强电源控制技术, 再辅以 802.11, 实际空闲功耗明显降低\n",
    "\n",
    "此外, 新的规范还加入 UCD 单向广播无连接数据技术, 提高了蓝牙设备的相应能力"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 第四代蓝牙：主推\"Low Energy\"低功耗\n",
    "#### 2010 年：蓝牙 4.0\n",
    "蓝牙 4.0 是迄今为止第一个蓝牙综合协议规范, 将三种规格集成在一起. 其中最重要的变化就是 BLE(Bluetooth Low Energy)低功耗功能, 提出了低功耗蓝牙、传统蓝牙和高速蓝牙三种模式:\n",
    "- 高速蓝牙: 主攻数据交换与传输; \n",
    "- 传统蓝牙: 以信息沟通、设备连接为重点; \n",
    "- 低功耗蓝牙: 以不需占用太多带宽的设备连接为主, 功耗较老版本降低了 90%\n",
    "\n",
    "蓝牙 4.0 的芯片模式分为 Single mode 与 Dual mode. Single mode 只能与蓝牙 4.0 互相传输无法向下与 3.0/2.1/2.0 版本兼容; Dual mode 可以向下兼容 3.0/2.1/2.0 版本. 前者应用于使用纽扣电池的传感器设备; 后者应用于传统蓝牙设备, 同时兼顾低功耗的需求\n",
    "\n",
    "此外, 蓝牙 4.0 还把蓝牙的传输距离提升到100米以上(低功耗模式条件下). 拥有更快的响应速度, 最短可在 3 毫秒内完成连接设置并开始传输数据. 更安全的技术, 使用 AES-128 CCM 加密算法进行数据包加密和认证\n",
    "\n",
    "苹果 iPhone 4S 是第一款支持蓝牙 4.0 标准的智能手机\n",
    "#### 2013 年: 蓝牙 4.1\n",
    "蓝牙 4.1 在传输速度和传输范围上变化很小, 此次更新目的是为了让 Bluetooth Smart 技术最终成为物联网(Internet of Things)发展的核心动力;\n",
    "\n",
    "支持与 LTE 无缝协作. 当蓝牙与 LTE 无线电信号同时传输数据时, 那么蓝牙 4.1 可以自动协调两者的传输信息, 以确保协同传输, 降低相互干扰;\n",
    "\n",
    "允许开发人员和制造商\"自定义\"蓝牙 4.1 设备的重新连接间隔, 为开发人员提供了更高的灵活性和掌控度; \n",
    "\n",
    "支持\"云同步\". 蓝牙 4.1 加入了专用的 IPv6 通道, 蓝牙 4.1 设备只需要连接到可以联网的设备(如手机), 就可以通过 IPv6 与云端的数据进行同步, 满足物联网的应用需求; --> 小米蓝牙网关\n",
    "\n",
    "支持\"扩展设备\"与\"中心设备\"角色互换. 支持蓝牙 4.1 标准的耳机、手表、键鼠, 可以不用通过 PC、平板、手机等数据枢纽, 实现自主收发数据. \n",
    "\n",
    "#### 2014 年: 蓝牙 4.2\n",
    "蓝牙 4.2 的传输速度更加快速, 比上代提高了 2.5 倍, 因为蓝牙智能(Bluetooth Smart)数据包的容量提高, 其可容纳的数据量相当于此前的10倍左右;\n",
    "\n",
    "改善了传输速率和隐私保护程度, 蓝牙信号想要连接或者追踪用户设备, 必须经过用户许可.\n",
    "\n",
    "支持 6LoWPAN, 6LoWPAN 是一种基于 IPv6 的低速无线个域网标准. 蓝牙 4.2 设备可以直接通过 IPv6 和 6LoWPAN 接入互联网. 这一技术允许多个蓝牙设备通过一个终端接入互联网或者局域网;\n",
    "![](./images/blueSpeed.png)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 第五代蓝牙: 开启\"物联网\"时代大门\n",
    "#### 2016 年: 蓝牙 5.0\n",
    "蓝牙 5.0 在低功耗模式下具备更快更远的传输能力, 传输速率是蓝牙 4.2 的两倍(速度上限为 2Mbps), 有效传输距离是蓝牙 4.2 的四倍(理论上可达 300 米), 数据包容量是蓝牙 4.2 的八倍\n",
    "\n",
    "支持室内定位导航功能, 结合 WiFi 可以实现精度小于 1 米的室内定位\n",
    "![](./images/duibi.png)\n",
    "\n",
    "#### Mesh 网状网络：实现物联网的关键\"钥匙\"\n",
    "Mesh 网状网络是一项独立研发的网络技术, 它能够将蓝牙设备作为信号中继站, 将数据覆盖到非常大的物理区域, 兼容蓝牙 4 和 5 系列的协议;\n",
    "传统的蓝牙连接是通过一台设备到另一台设备的\"配对\"实现的, 建立\"一对一\"或\"一对多\"的微型网络关系;\n",
    "而 Mesh 网络能够使设备实现「多对多」的关系. Mesh 网络中每个设备节点都能发送和接收信息, 只要有一个设备连上网关, 信息就能够在节点之间被中继, 从而让消息传输至比无线电波正常传输距离更远的位置;"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 二、工作方式:\n",
    "## 1.基础率/增强数据率(BR/EDR):\n",
    "以点对点网络拓扑结构建立一对一设备通信;\n",
    "\n",
    "## 2. 低耗能(LE):\n",
    "使用点对点(一对一)、广播(一对多)和网格(多对多)等多种网络拓扑结构"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. 典型蓝牙与低耗电蓝牙（4.0）:\n",
    "|技术规范|\t典型蓝牙|\t低耗电蓝牙|\n",
    "|-|-|-|\n",
    "|无线电频率|\t2.4 GHz|\t2.4 GHz|\n",
    "|距离\t|10米/100米|\t30米|\n",
    "|空中数据速率\t|1-3 Mb/s|\t1 Mb/s|\n",
    "|应用吞吐量\t|0.7-2.1 Mb/s\t|0.2 Mb/s|\n",
    "|节点／单元\t|7-16,777,184\t|未定义(理论最大值为2^32)|\n",
    "|安全\t|64/128-bit及用户自定义的应用层\t|128-bit AES及用户自定义的应用层|\n",
    "|强健性|\t自动适应快速跳频, FEC, 快速ACK\t|自动适应快速跳频\n",
    "|延迟(非连接状态)\t|100 ms|\t<6 ms|\n",
    "|发送数据的总时间\t|0.625 ms\t|3 ms|\n",
    "|语音能力|\t有|\t没有|\n",
    "|网络拓扑|\t分散网|\t星状拓扑(Star) 汇流排拓扑(Bus) 网状拓扑(Mesh)\n",
    "|耗电量\t|1(作为参考)|\t0.01至0.5(视使用情况)|\n",
    "|最大操作电流|\t<30 mA|\t<15 mA(最高运行时为15 mA)|\n",
    "|主要用途\t|手机, 游戏机, 耳机, 立体声音频串流, 汽车和PC等|手机, 游戏机, PC, 表, 体育和健身, 医疗保健, 汽车, 家用电子, 自动化和工业等|"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 三、开发蓝牙连接:\n",
    "## 1. 标准蓝牙:\n",
    "主机协议栈支持的经典蓝牙协议规范和协议如下：\n",
    "- 规范：GAP、A2DP(SNK)、AVRCP(CT)\n",
    "- 协议：L2CAP、SDP、AVDTP、AVCTP\n",
    "\n",
    "协议和规范\n",
    "- L2CAP:\n",
    "    - 蓝牙逻辑链路控制和适配协议;\n",
    "- L2CAP:\n",
    "    - 信道共支持6种模式：基本L2CAP模式、流量控制模式、重传模式、加强重传模式、流模式、基于LE Credit的流量控制模式\n",
    "- SDP 服务发现协议: \n",
    "    - 允许应用程序发现其他对等蓝牙设备提供的服务, 并确定可用服务的特征. \n",
    "- GAP: 通用访问规范\n",
    "    - 可提供有关设备可发现性、可连接性和安全性的模式和过程描述. \n",
    "- A2DP和AVRCP:\n",
    "    - 高级音频分发规范、音频/视频远程控制规范\n",
    "\n",
    "## 2. 低功耗蓝牙:\n",
    "GAP协议层定义了 BLE 设备的发现流程, 设备管理理和设备连接的建立;\n",
    "设备的发现通过通用访问配置文件(GAP)协议进行.  发现之后, 设备到设备之间的通信通过通用属性 (GATT)协议进行;\n",
    "[微软的文档](https://docs.microsoft.com/zh-cn/windows/uwp/devices-sensors/bluetooth-low-energy-overview)中给出了服务关系:\n",
    "![](./images/ms_ble.png)\n",
    "心率服务以 GATT 服务器 API 格式表示"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. 蓝牙开发必须知道的概念\n",
    "### 3.1 central和peripheral\n",
    "蓝牙应用开发中, 存在两种角色, 分别是central和peripheral(pə’rɪfərəl) ,中文就是中心和外设. 比如手机去连接智能设备, 那手机就是central, 智能设备就是peripheral. 大多时候都是central去连接peripheral的场景;\n",
    "\n",
    "### 3.2 广播和连接\n",
    "peripheral会发出广播(advertisement:ædvɚ’taɪzmənt),central扫描到广播后, 可以对设备进行连接, 发出connect请求, peripheral接收到请求后, 同意连接后, central和peripheral就建立了连接. \n",
    "\n",
    "### 3.3 连接后的操作\n",
    "\n",
    "write, read, notify, indecate,  response or not … \n",
    "indecate和notify的区别就在于, indecate是一定会收到数据, notify有可能会丢失数据(不会有central收到数据的回应), write也分为response和noresponse, 如果是response, 那么write成功回收到peripheral的确认消息, 但是会降低写入的速率. \n",
    "\n",
    "### 3.4 数据协议\n",
    "每个具体的智能设备, 都约定了一组数据格式, 这个就是数据协议, 例如小米蓝牙温度计获取道德温湿度数据是0xF90719DF0B, 其中第1到第4位表示温度, 即0x07F9,转换成十进制是2041, 表示20.41℃, 第5,6位表示湿度, 即0x19, 转换成十进制是25, 表示25%. 需要注意的是, 设备端都是小端模式, 所以取4位时候, 高字节在前低字节在后"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. 低功耗蓝牙(BLE)协议栈：\n",
    "### 4.1 协议栈框架：\n",
    "要实现一个BLE应用, 首先需要一个支持BLE射频的芯片, 然后还需要提供一个与此芯片配套的BLE协议栈, 最后在协议栈上开发自己的应用. 简单来说, BLE协议栈主要用来对应用数据进行层层封包, 以生成一个满足BLE协议的空中数据包, 也就是说, 把应用数据包裹在一系列的帧头(header)和帧尾(tail)中. 具体来说, BLE协议栈主要由如下几部分组成:\n",
    "- PHY层(Physical layer物理层):\n",
    "    - 指定BLE所用的无线频段,调制解调方式和方法等\n",
    "- LL层(Link Layer链路层):\n",
    "    - 是整个BLE协议栈的核心.LL层只负责把数据发出去或者收回来, 对数据进行怎样的解析则交给上面的GAP或者GATT.\n",
    "- HCI(Host controller interface):\n",
    "    - 用来规范两者之间的通信协议和通信命令.\n",
    "- GAP层(Generic access profile):\n",
    "    - GAP是对LL层payload(有效数据包)如何进行解析的两种方式中的一种,而且是最简单的那一种. GAP目前主要用来进行广播,扫描和发起连接等;\n",
    "- L2CAP层(Logic link control and adaptation protocol):\n",
    "    - L2CAP对LL进行了一次简单封装,LL只关心传输的数据本身,L2CAP就要区分是加密通道还是普通通道,同时还要对连接间隔进行管理.\n",
    "- SMP(Secure manager protocol):\n",
    "    - SMP用来管理BLE连接的加密和安全的,如何保证连接的安全性,同时不影响用户的体验,这些都是SMP要考虑的工作.\n",
    "- ATT(Attribute protocol):\n",
    "    - ATT层用来定义用户命令及命令操作的数据. BLE协议栈中,开发者接触最多的就是ATT. BLE引入了attribute概念, 用来描述一条一条的数据. Attribute除了定义数据, 同时定义该数据可以使用的ATT命令,因此这一层被称为ATT层.\n",
    "- GATT(Generic attribute profile ):\n",
    "    - GATT用来规范attribute中的数据内容,并运用group(分组)的概念对attribute进行分类管理.没有GATT,BLE协议栈也能跑,但互联互通就会出问题,也正是因为有了GATT和各种各样的应用profile,BLE摆脱了ZigBee(Zigbee技术是一种应用于短距离和低速率下的无线通信技术)等无线协议的兼容性困境,成了出货量最大的2.4G无线通信产品"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. 蓝牙应用的一般开发流程\n",
    "- 建立中心角色\n",
    "- 扫描外设(discover)\n",
    "- 连接外设(connect)\n",
    "- 扫描外设中的服务和特征(discover) \n",
    "    - 获取外设的services\n",
    "    - 获取外设的Characteristics,获取Characteristics的值, 获取Characteristics的Descriptor和Descriptor的值\n",
    "- 与外设做数据交互(explore and interact)\n",
    "- 订阅Characteristic的通知\n",
    "- 断开连接(disconnect)\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 四、python连接蓝牙的常见库:\n",
    "## 1. pybluez:\n",
    "- Document: [https://pybluez.github.io/](https://pybluez.github.io/)\n",
    "- github: [https://github.com/pybluez/pybluez](https://github.com/pybluez/pybluez)\n",
    "- 注意:\n",
    "    - 仅GNU/Linux实验性支持 BLE"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. bluepy:\n",
    "- pypi: [https://pypi.org/project/bluepy/](https://pypi.org/project/bluepy/)\n",
    "- github:[https://github.com/IanHarvey/bluepy](https://github.com/IanHarvey/bluepy)\n",
    "- 树莓派上的支持: [https://www.elinux.org/RPi_Bluetooth_LE](https://www.elinux.org/RPi_Bluetooth_LE)\n",
    "- 注意:\n",
    "    - 目前，它仅在Linux上运行"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. pygatt:\n",
    "- pypi: [https://pypi.org/project/pygatt/](https://pypi.org/project/pygatt/)\n",
    "- github:[https://github.com/peplin/pygatt](https://github.com/peplin/pygatt)\n",
    "- 注意:\n",
    "    - pygatt通过包装两个不同的后端来提供Pythonic API：\n",
    "        - BlueZ(需要Linux), 使用gatttool命令行实用程序;\n",
    "        - Bluegiga的BGAPI与BLED112等USB适配器兼容;"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. from socket import AF_BLUETOOTH\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. bleak:\n",
    "- pypi: [https://pypi.org/project/bleak/](https://pypi.org/project/bleak/)\n",
    "- Document: [https://bleak.readthedocs.io/en/latest/](https://bleak.readthedocs.io/en/latest/)\n",
    "\n",
    "- github: [https://github.com/hbldh/bleak](https://github.com/hbldh/bleak)\n",
    "- 说明:\n",
    "    - Bleak是GATT客户端软件, 能够连接到充当GATT服务器的BLE设备. 它旨在提供异步的, 跨平台的Python API, 以与例如传感器进行连接和通信;\n",
    "    - 需要asyncio, 支持python3"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 五、演示:\n",
    "## 1. 小米蓝牙温度计:\n",
    "# 对米家蓝牙温度计2:\n",
    "\n",
    "\n",
    "![](./images/mijia2_ser.jpg)\n",
    "![](./images/mijia2_notify.jpg)\n",
    "\n",
    "- [米家BLE产品接入](https://iot.mi.com/new/doc/embedded-development/ble/introduction.html)\n",
    "- [米家BLE Object定义](https://iot.mi.com/new/doc/embedded-development/ble/object-definition.html)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "import asyncio\n",
    "from bleak import discover\n",
    "from bleak import BleakClient"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {},
   "outputs": [],
   "source": [
    "DEVICE_NAME = \"LYWSD03MMC\"\n",
    "\n",
    "MAC_ADDRESS = \"A4:C1:38:45:68:F1\"\n",
    "\n",
    "SERVICE_UUID = \"ebe0ccb0-7a0a-4b0c-8a1a-6ff2997da3a6\"\n",
    "MODEL_NBR_UUID = \"ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6\" # 53\n",
    "BATTERY_UUID = \"00002a19-0000-1000-8000-00805f9b34fb\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def scan_devices():\n",
    "    \"\"\"\n",
    "    扫描设备:\n",
    "    \"\"\"\n",
    "    devices = await discover()\n",
    "    for d in devices:\n",
    "        print(d.address, d.name, d.rssi)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def measure_battery(address, loop):\n",
    "    \"\"\"\n",
    "    获取电池电量\n",
    "    \"\"\"\n",
    "    async with BleakClient(address, loop=loop) as client:\n",
    "        battery_level  = await client.read_gatt_char(BATTERY_UUID)\n",
    "        print(\"Battery Level: {0}%\".format(int(battery_level[0])))\n",
    "        return int(battery_level[0])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "async def connect_device(address, loop):\n",
    "    for i in range(10):\n",
    "        try:\n",
    "            async with BleakClient(address, loop=loop) as client:\n",
    "                await client.get_services()\n",
    "                await client.start_notify(MODEL_NBR_UUID, callback)\n",
    "                print(\"订阅成功\")\n",
    "                for i in range(5):\n",
    "                    # battery_level  = await client.read_gatt_char(BATTERY_UUID)\n",
    "                    # print(\"Battery Level: {0}%\".format(int(battery_level[0])))\n",
    "                    await asyncio.sleep(6)\n",
    "                if await disconnect():\n",
    "                    await client.stop_notify(MODEL_NBR_UUID)\n",
    "                    await client.disconnect()\n",
    "                return\n",
    "        except Exception as e:\n",
    "            print(e)        "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "def callback(sender, data):\n",
    "    data_list = [format(i, 'x')   for i in data]\n",
    "    temp = int(f'0X{data_list[1]}{data_list[0]}',16)/100.0\n",
    "    sd = int(f'0x{data_list[2]}',16)\n",
    "    print(f\"{sender}: 温度:{temp}℃, 湿度:{sd}%\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "loop = asyncio.get_event_loop()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "ename": "RuntimeError",
     "evalue": "This event loop is already running",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mRuntimeError\u001b[0m                              Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-8-6445e571db3c>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mloop\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrun_until_complete\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mscan_devices\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;31m# 扫描设备\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;32mC:\\Develop\\Python\\Anaconda3\\lib\\asyncio\\base_events.py\u001b[0m in \u001b[0;36mrun_until_complete\u001b[1;34m(self, future)\u001b[0m\n\u001b[0;32m    569\u001b[0m         \u001b[0mfuture\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0madd_done_callback\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0m_run_until_complete_cb\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    570\u001b[0m         \u001b[1;32mtry\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 571\u001b[1;33m             \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrun_forever\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    572\u001b[0m         \u001b[1;32mexcept\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    573\u001b[0m             \u001b[1;32mif\u001b[0m \u001b[0mnew_task\u001b[0m \u001b[1;32mand\u001b[0m \u001b[0mfuture\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdone\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mand\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mfuture\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcancelled\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
      "\u001b[1;32mC:\\Develop\\Python\\Anaconda3\\lib\\asyncio\\base_events.py\u001b[0m in \u001b[0;36mrun_forever\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    524\u001b[0m         \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_check_closed\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    525\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mis_running\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 526\u001b[1;33m             \u001b[1;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'This event loop is already running'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    527\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[0mevents\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_get_running_loop\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    528\u001b[0m             raise RuntimeError(\n",
      "\u001b[1;31mRuntimeError\u001b[0m: This event loop is already running"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "58:91:92:89:BC:74 Unknown -84\n",
      "2A:F4:95:F2:4C:5B Unknown -56\n",
      "62:46:B5:D9:35:84 Unknown -84\n",
      "60:7F:85:C4:7D:B6 Unknown -92\n"
     ]
    }
   ],
   "source": [
    "loop.run_until_complete(scan_devices()) # 扫描设备"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [
    {
     "ename": "RuntimeError",
     "evalue": "This event loop is already running",
     "output_type": "error",
     "traceback": [
      "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m",
      "\u001b[1;31mRuntimeError\u001b[0m                              Traceback (most recent call last)",
      "\u001b[1;32m<ipython-input-9-c805e9bc6d51>\u001b[0m in \u001b[0;36m<module>\u001b[1;34m\u001b[0m\n\u001b[1;32m----> 1\u001b[1;33m \u001b[0mloop\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrun_until_complete\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mconnect_device\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0mMAC_ADDRESS\u001b[0m \u001b[1;33m,\u001b[0m \u001b[0mloop\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m",
      "\u001b[1;32mC:\\Develop\\Python\\Anaconda3\\lib\\asyncio\\base_events.py\u001b[0m in \u001b[0;36mrun_until_complete\u001b[1;34m(self, future)\u001b[0m\n\u001b[0;32m    569\u001b[0m         \u001b[0mfuture\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0madd_done_callback\u001b[0m\u001b[1;33m(\u001b[0m\u001b[0m_run_until_complete_cb\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    570\u001b[0m         \u001b[1;32mtry\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 571\u001b[1;33m             \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mrun_forever\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    572\u001b[0m         \u001b[1;32mexcept\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    573\u001b[0m             \u001b[1;32mif\u001b[0m \u001b[0mnew_task\u001b[0m \u001b[1;32mand\u001b[0m \u001b[0mfuture\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mdone\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mand\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[0mfuture\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mcancelled\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n",
      "\u001b[1;32mC:\\Develop\\Python\\Anaconda3\\lib\\asyncio\\base_events.py\u001b[0m in \u001b[0;36mrun_forever\u001b[1;34m(self)\u001b[0m\n\u001b[0;32m    524\u001b[0m         \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_check_closed\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    525\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[0mself\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0mis_running\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[1;32m--> 526\u001b[1;33m             \u001b[1;32mraise\u001b[0m \u001b[0mRuntimeError\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;34m'This event loop is already running'\u001b[0m\u001b[1;33m)\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0m\u001b[0;32m    527\u001b[0m         \u001b[1;32mif\u001b[0m \u001b[0mevents\u001b[0m\u001b[1;33m.\u001b[0m\u001b[0m_get_running_loop\u001b[0m\u001b[1;33m(\u001b[0m\u001b[1;33m)\u001b[0m \u001b[1;32mis\u001b[0m \u001b[1;32mnot\u001b[0m \u001b[1;32mNone\u001b[0m\u001b[1;33m:\u001b[0m\u001b[1;33m\u001b[0m\u001b[1;33m\u001b[0m\u001b[0m\n\u001b[0;32m    528\u001b[0m             raise RuntimeError(\n",
      "\u001b[1;31mRuntimeError\u001b[0m: This event loop is already running"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Device with address A4:C1:38:45:68:F1 was not found.\n",
      "订阅成功\n",
      "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6: 温度:24.29℃, 湿度:32%\n",
      "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6: 温度:24.28℃, 湿度:32%\n",
      "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6: 温度:24.29℃, 湿度:32%\n",
      "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6: 温度:24.28℃, 湿度:32%\n",
      "ebe0ccc1-7a0a-4b0c-8a1a-6ff2997da3a6: 温度:24.28℃, 湿度:32%\n",
      "'NoneType' object has no attribute 'Dispose'\n",
      "Device with address A4:C1:38:45:68:F1 was not found.\n",
      "Device with address A4:C1:38:45:68:F1 was not found.\n",
      "Device with address A4:C1:38:45:68:F1 was not found.\n",
      "Device with address A4:C1:38:45:68:F1 was not found.\n",
      "Device with address A4:C1:38:45:68:F1 was not found.\n",
      "Device with address A4:C1:38:45:68:F1 was not found.\n",
      "Device with address A4:C1:38:45:68:F1 was not found.\n",
      "Device with address A4:C1:38:45:68:F1 was not found.\n"
     ]
    }
   ],
   "source": [
    "loop.run_until_complete(connect_device(MAC_ADDRESS , loop))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. C# framework:\n",
    "- 微软官方文档:[https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice](https://docs.microsoft.com/en-us/uwp/api/windows.devices.bluetooth.bluetoothledevice)\n",
    "\n",
    "- **注:**\n",
    "    - C# framework 实现的小工具"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3.esp32:\n",
    "![](./images/esp32.jpg)\n",
    "- 环境: esp32单片机+micropython\n",
    "- 目的: 拿到esp32温度传感器的温度值,消息/可读\n",
    "- MicroPython官网: [http://www.micropython.org/](http://www.micropython.org/)\n",
    "- MicroPython Document:  [MicroPython](http://docs.micropython.org/en/latest/esp32/quickref.html)\n",
    "- MicroPython Github: [https://github.com/micropython/micropython](https://github.com/micropython/micropython)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3.1 esp32 详情:\n",
    "- 架构：Xtensa双核32位LX6\n",
    "- CPU频率：高达240MHz\n",
    "- 可用总内存：528KB（部分保留给系统使用）\n",
    "- 引导ROM：448KB\n",
    "- 外部FlashROM：通过SPI Flash的代码和数据；通常大小4MB\n",
    "- GPIO：34（GPIO与其他功能复用，包括外部FlashROM，UART等）\n",
    "- UART：3个RX / TX UART（无硬件握手），一个仅TX的UART\n",
    "- SPI：4个SPI接口（其中一个用于FlashROM）\n",
    "- I2C：2个I2C（可在任何引脚上使用bitbang实现）\n",
    "- I2S：2\n",
    "- ADC：最多18个通道的12位SAR ADC\n",
    "- DAC：2个8位DAC\n",
    "- RMT：8个通道，可进行精确的脉冲发送/接收\n",
    "- 编程：使用来自UART的BootROM引导加载程序-由于外部FlashROM和始终可用的BootROM引导加载程序，ESP32不能固定\n",
    "\n",
    "![](./images/esp32_pins.png)\n",
    "\n",
    "### 3.2 实现:\n",
    "- MicroPython 蓝牙文档: [http://docs.micropython.org/en/latest/library/ubluetooth.html?highlight=ble#](http://docs.micropython.org/en/latest/library/ubluetooth.html?highlight=ble#)\n",
    "\n",
    "![](./images/esp32+ch05.jpg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {
    "scrolled": true
   },
   "outputs": [],
   "source": [
    "# ble_temperature.py \n",
    "# utf-8\n",
    "#这个例子演示了一个简单的温度传感器外围设备。\n",
    "#\n",
    "#传感器的本地值每秒更新一次，它将通知\n",
    "#每10秒连接一个中心。\n",
    "\n",
    "import bluetooth\n",
    "import random\n",
    "import struct\n",
    "import time\n",
    "from micropython import const\n",
    "import esp32\n",
    "\n",
    "_IRQ_CENTRAL_CONNECT = const(1 << 0)\n",
    "_IRQ_CENTRAL_DISCONNECT = const(1 << 1)\n",
    "\n",
    "# 定义蓝牙服务: 环境感知\n",
    "_ENV_SENSE_UUID = bluetooth.UUID(0x181A)\n",
    "# 定义蓝牙特征: 温度(可读/通知)\n",
    "_TEMP_CHAR = (\n",
    "    bluetooth.UUID(0x2A6E),\n",
    "    bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,\n",
    ")\n",
    "_ENV_SENSE_SERVICE = (\n",
    "    _ENV_SENSE_UUID,\n",
    "    (_TEMP_CHAR,),\n",
    ")\n",
    "# 定义蓝牙GAP应用\n",
    "_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768)\n",
    "\n",
    "_ADV_TYPE_FLAGS = const(0x01)\n",
    "_ADV_TYPE_NAME = const(0x09)\n",
    "_ADV_TYPE_UUID16_COMPLETE = const(0x3)\n",
    "_ADV_TYPE_UUID32_COMPLETE = const(0x5)\n",
    "_ADV_TYPE_UUID128_COMPLETE = const(0x7)\n",
    "_ADV_TYPE_UUID16_MORE = const(0x2)\n",
    "_ADV_TYPE_UUID32_MORE = const(0x4)\n",
    "_ADV_TYPE_UUID128_MORE = const(0x6)\n",
    "_ADV_TYPE_APPEARANCE = const(0x19)\n",
    "\n",
    "\n",
    "# 生成要传递给gap_advertise的负载(adv_data=...).\n",
    "def advertising_payload(limited_disc=False, br_edr=False, name=None, services=None, appearance=0):\n",
    "    payload = bytearray()\n",
    "\n",
    "    def _append(adv_type, value):\n",
    "        nonlocal payload\n",
    "        payload += struct.pack(\"BB\", len(value) + 1, adv_type) + value\n",
    "\n",
    "    _append(\n",
    "        _ADV_TYPE_FLAGS,\n",
    "        struct.pack(\"B\", (0x01 if limited_disc else 0x02) + (0x00 if br_edr else 0x04)),\n",
    "    )\n",
    "\n",
    "    if name:\n",
    "        _append(_ADV_TYPE_NAME, name)\n",
    "\n",
    "    if services:\n",
    "        for uuid in services:\n",
    "            b = bytes(uuid)\n",
    "            if len(b) == 2:\n",
    "                _append(_ADV_TYPE_UUID16_COMPLETE, b)\n",
    "            elif len(b) == 4:\n",
    "                _append(_ADV_TYPE_UUID32_COMPLETE, b)\n",
    "            elif len(b) == 16:\n",
    "                _append(_ADV_TYPE_UUID128_COMPLETE, b)\n",
    "\n",
    "    # See org.bluetooth.characteristic.gap.appearance.xml\n",
    "    _append(_ADV_TYPE_APPEARANCE, struct.pack(\"<h\", appearance))\n",
    "\n",
    "    return payload\n",
    "\n",
    "\n",
    "class BLETemperature:\n",
    "    def __init__(self, ble, name=\"mpy-temp\"):\n",
    "        self._ble = ble\n",
    "        self._ble.active(True)\n",
    "        self._ble.irq(handler=self._irq)\n",
    "        ((self._handle,),) = self._ble.gatts_register_services((_ENV_SENSE_SERVICE,))\n",
    "        self._connections = set()\n",
    "        self._payload = advertising_payload(\n",
    "            name=name, services=[_ENV_SENSE_UUID], appearance=_ADV_APPEARANCE_GENERIC_THERMOMETER\n",
    "        )\n",
    "        self._advertise()\n",
    "\n",
    "    def _irq(self, event, data):\n",
    "        \"\"\"\n",
    "        跟踪连接以便可以发送通知\n",
    "        \"\"\"\n",
    "        if event == _IRQ_CENTRAL_CONNECT:\n",
    "            conn_handle, _, _, = data\n",
    "            self._connections.add(conn_handle)\n",
    "        elif event == _IRQ_CENTRAL_DISCONNECT:\n",
    "            conn_handle, _, _, = data\n",
    "            self._connections.remove(conn_handle)\n",
    "            # 重新开始广告以允许新连接\n",
    "            self._advertise()\n",
    "\n",
    "    def set_temperature(self, temp_deg_c, notify=False):\n",
    "        \"\"\"\n",
    "        数据以摄氏度表示为sint16，分辨率为0.01摄氏度\n",
    "        写入本地值，以备central读取。\n",
    "        \"\"\"\n",
    "        self._ble.gatts_write(self._handle, struct.pack(\"<h\", int(temp_deg_c * 100)))\n",
    "        if notify:\n",
    "            for conn_handle in self._connections:\n",
    "                # Notify connected centrals to issue a read\n",
    "                self._ble.gatts_notify(conn_handle, self._handle)\n",
    "\n",
    "    def _advertise(self, interval_us=500000):\n",
    "        self._ble.gap_advertise(interval_us, adv_data=self._payload)\n",
    "\n",
    "\n",
    "def demo():\n",
    "    ble = bluetooth.BLE()\n",
    "    temp = BLETemperature(ble)\n",
    "\n",
    "    t = 25\n",
    "    i = 0\n",
    "\n",
    "    while True:\n",
    "        # 每秒钟写一次，每10秒通知一次\n",
    "        i = (i + 1) % 5\n",
    "        t = esp32.raw_temperature() # 获取内部的华氏温度\n",
    "        print(t)\n",
    "        temp.set_temperature(t, notify=i == 0)\n",
    "        time.sleep_ms(1000)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    demo()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "![](./images/esp32_temper.jpg)\n",
    "\n",
    "![](./images/ble_tool.jpg)\n",
    "\n",
    "![](./images/bletool_notify.jpg)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "72:C7:90:E9:D2:65 Unknown -83\n",
      "17:31:79:34:29:B5 Unknown -91\n",
      "7F:DB:F4:3A:A7:C6 Unknown -88\n",
      "7F:DA:52:CF:F6:54 Unknown -92\n",
      "5E:0B:25:F8:52:CB Unknown -93\n",
      "F4:5C:89:97:EF:82 Unknown -95\n"
     ]
    }
   ],
   "source": [
    "# 查找并连接到BLE温度传感器\n",
    "\n",
    "import bluetooth\n",
    "import random\n",
    "import struct\n",
    "import time\n",
    "import micropython\n",
    "\n",
    "from micropython import const\n",
    "\n",
    "_IRQ_CENTRAL_CONNECT = const(1 << 0)\n",
    "_IRQ_CENTRAL_DISCONNECT = const(1 << 1)\n",
    "_IRQ_GATTS_WRITE = const(1 << 2)\n",
    "_IRQ_GATTS_READ_REQUEST = const(1 << 3)\n",
    "_IRQ_SCAN_RESULT = const(1 << 4)\n",
    "_IRQ_SCAN_COMPLETE = const(1 << 5)\n",
    "_IRQ_PERIPHERAL_CONNECT = const(1 << 6)\n",
    "_IRQ_PERIPHERAL_DISCONNECT = const(1 << 7)\n",
    "_IRQ_GATTC_SERVICE_RESULT = const(1 << 8)\n",
    "_IRQ_GATTC_CHARACTERISTIC_RESULT = const(1 << 9)\n",
    "_IRQ_GATTC_DESCRIPTOR_RESULT = const(1 << 10)\n",
    "_IRQ_GATTC_READ_RESULT = const(1 << 11)\n",
    "_IRQ_GATTC_WRITE_STATUS = const(1 << 12)\n",
    "_IRQ_GATTC_NOTIFY = const(1 << 13)\n",
    "_IRQ_GATTC_INDICATE = const(1 << 14)\n",
    "_IRQ_ALL = const(0xFFFF)\n",
    "\n",
    "_ADV_IND = const(0x00)\n",
    "_ADV_DIRECT_IND = const(0x01)\n",
    "_ADV_SCAN_IND = const(0x02)\n",
    "_ADV_NONCONN_IND = const(0x03)\n",
    "\n",
    "# 蓝牙服务环境感知\n",
    "_ENV_SENSE_UUID = bluetooth.UUID(0x181A)\n",
    "# org.bluetooth.characteristic.temperature\n",
    "_TEMP_UUID = bluetooth.UUID(0x2A6E)\n",
    "_TEMP_CHAR = (\n",
    "    _TEMP_UUID,\n",
    "    bluetooth.FLAG_READ | bluetooth.FLAG_NOTIFY,\n",
    ")\n",
    "_ENV_SENSE_SERVICE = (\n",
    "    _ENV_SENSE_UUID,\n",
    "    (_TEMP_CHAR,),\n",
    ")\n",
    "\n",
    "# 定义蓝牙GAP应用\n",
    "_ADV_APPEARANCE_GENERIC_THERMOMETER = const(768)\n",
    "\n",
    "_ADV_TYPE_FLAGS = const(0x01)\n",
    "_ADV_TYPE_NAME = const(0x09)\n",
    "_ADV_TYPE_UUID16_COMPLETE = const(0x3)\n",
    "_ADV_TYPE_UUID32_COMPLETE = const(0x5)\n",
    "_ADV_TYPE_UUID128_COMPLETE = const(0x7)\n",
    "_ADV_TYPE_UUID16_MORE = const(0x2)\n",
    "_ADV_TYPE_UUID32_MORE = const(0x4)\n",
    "_ADV_TYPE_UUID128_MORE = const(0x6)\n",
    "_ADV_TYPE_APPEARANCE = const(0x19)\n",
    "\n",
    "def decode_name(payload):\n",
    "    n = decode_field(payload, _ADV_TYPE_NAME)\n",
    "    return str(n[0], \"utf-8\") if n else \"\"\n",
    "\n",
    "def decode_services(payload):\n",
    "    services = []\n",
    "    for u in decode_field(payload, _ADV_TYPE_UUID16_COMPLETE):\n",
    "        services.append(bluetooth.UUID(struct.unpack(\"<h\", u)[0]))\n",
    "    for u in decode_field(payload, _ADV_TYPE_UUID32_COMPLETE):\n",
    "        services.append(bluetooth.UUID(struct.unpack(\"<d\", u)[0]))\n",
    "    for u in decode_field(payload, _ADV_TYPE_UUID128_COMPLETE):\n",
    "        services.append(bluetooth.UUID(u))\n",
    "    return services\n",
    "\n",
    "class BLETemperatureCentral:\n",
    "    def __init__(self, ble):\n",
    "        self._ble = ble\n",
    "        self._ble.active(True)\n",
    "        self._ble.irq(handler=self._irq)\n",
    "\n",
    "        self._reset()\n",
    "\n",
    "    def _reset(self):\n",
    "        \"\"\"\n",
    "        成功扫描后缓存的名称和地址\n",
    "        \"\"\"\n",
    "        self._name = None\n",
    "        self._addr_type = None\n",
    "        self._addr = None\n",
    "\n",
    "        # Cached value 如果有的话\n",
    "        self._value = None\n",
    "\n",
    "        #完成各种操作的回调。\n",
    "        #这些在被调用后重置为“无”。\n",
    "        self._scan_callback = None\n",
    "        self._conn_callback = None\n",
    "        self._read_callback = None\n",
    "\n",
    "        # 从设备通知新数据时的持久回调\n",
    "        self._notify_callback = None\n",
    "\n",
    "        # 连接设备\n",
    "        self._conn_handle = None\n",
    "        self._value_handle = None\n",
    "\n",
    "    def _irq(self, event, data):\n",
    "        if event == _IRQ_SCAN_RESULT:\n",
    "            addr_type, addr, adv_type, rssi, adv_data = data\n",
    "            if adv_type in (_ADV_IND, _ADV_DIRECT_IND,) and _ENV_SENSE_UUID in decode_services(\n",
    "                adv_data\n",
    "            ):\n",
    "                # 找到一个潜在的设备，记住它并停止扫描\n",
    "                self._addr_type = addr_type\n",
    "                self._addr = bytes(\n",
    "                    addr\n",
    "                )  # #注意：addr缓冲区归调用方所有，因此需要复制它.\n",
    "                self._name = decode_name(adv_data) or \"?\"\n",
    "                self._ble.gap_scan(None)\n",
    "\n",
    "        elif event == _IRQ_SCAN_COMPLETE:\n",
    "            if self._scan_callback:\n",
    "                if self._addr:\n",
    "                    # 在扫描期间找到一个设备（扫描已显式停止）\n",
    "                    self._scan_callback(self._addr_type, self._addr, self._name)\n",
    "                    self._scan_callback = None\n",
    "                else:\n",
    "                    # Scan timed out.\n",
    "                    self._scan_callback(None, None, None)\n",
    "\n",
    "        elif event == _IRQ_PERIPHERAL_CONNECT:\n",
    "            # Connect successful.\n",
    "            conn_handle, addr_type, addr, = data\n",
    "            if addr_type == self._addr_type and addr == self._addr:\n",
    "                self._conn_handle = conn_handle\n",
    "                self._ble.gattc_discover_services(self._conn_handle)\n",
    "\n",
    "        elif event == _IRQ_PERIPHERAL_DISCONNECT:\n",
    "            # Disconnect \n",
    "            conn_handle, _, _, = data\n",
    "            if conn_handle == self._conn_handle:\n",
    "                # If it was initiated by us, it'll already be reset.\n",
    "                self._reset()\n",
    "\n",
    "        elif event == _IRQ_GATTC_SERVICE_RESULT:\n",
    "            # 连接成功返回服务.\n",
    "            conn_handle, start_handle, end_handle, uuid = data\n",
    "            if conn_handle == self._conn_handle and uuid == _ENV_SENSE_UUID:\n",
    "                self._ble.gattc_discover_characteristics(\n",
    "                    self._conn_handle, start_handle, end_handle\n",
    "                )\n",
    "\n",
    "        elif event == _IRQ_GATTC_CHARACTERISTIC_RESULT:\n",
    "            # 连接的设备返回了一个characteristic.\n",
    "            conn_handle, def_handle, value_handle, properties, uuid = data\n",
    "            if conn_handle == self._conn_handle and uuid == _TEMP_UUID:\n",
    "                self._value_handle = value_handle\n",
    "                # 连接和发现设备完毕，启动连接回调\n",
    "                if self._conn_callback:\n",
    "                    self._conn_callback()\n",
    "\n",
    "        elif event == _IRQ_GATTC_READ_RESULT:\n",
    "            # A read completed successfully.\n",
    "            conn_handle, value_handle, char_data = data\n",
    "            if conn_handle == self._conn_handle and value_handle == self._value_handle:\n",
    "                self._update_value(char_data)\n",
    "                if self._read_callback:\n",
    "                    self._read_callback(self._value)\n",
    "                    self._read_callback = None\n",
    "\n",
    "        elif event == _IRQ_GATTC_NOTIFY:\n",
    "            # 定期通知其值。\n",
    "            conn_handle, value_handle, notify_data = data\n",
    "            if conn_handle == self._conn_handle and value_handle == self._value_handle:\n",
    "                self._update_value(notify_data)\n",
    "                if self._notify_callback:\n",
    "                    self._notify_callback(self._value)\n",
    "\n",
    "    def is_connected(self):\n",
    "        \"\"\"\n",
    "        如果已成功连接并发现characteristics，则返回true\n",
    "        \"\"\"\n",
    "        return self._conn_handle is not None and self._value_handle is not None\n",
    "\n",
    "    \n",
    "    def scan(self, callback=None):\n",
    "        \"\"\"\n",
    "        寻找一个设备广告的环境传感器服务\n",
    "        \"\"\"\n",
    "        self._addr_type = None\n",
    "        self._addr = None\n",
    "        self._scan_callback = callback\n",
    "        self._ble.gap_scan(2000, 30000, 30000)\n",
    "\n",
    "    \n",
    "    def connect(self, addr_type=None, addr=None, callback=None):\n",
    "        \"\"\"\n",
    "        连接到指定的设备（否则使用扫描中的缓存地址）\n",
    "        \"\"\"\n",
    "        self._addr_type = addr_type or self._addr_type\n",
    "        self._addr = addr or self._addr\n",
    "        self._conn_callback = callback\n",
    "        if self._addr_type is None or self._addr is None:\n",
    "            return False\n",
    "        self._ble.gap_connect(self._addr_type, self._addr)\n",
    "        return True\n",
    "\n",
    "\n",
    "    def disconnect(self):\n",
    "        \"\"\"\n",
    "        断开与当前设备的连接\n",
    "        \"\"\"\n",
    "        if not self._conn_handle:\n",
    "            return\n",
    "        self._ble.gap_disconnect(self._conn_handle)\n",
    "        self._reset()\n",
    "\n",
    "    def read(self, callback):\n",
    "        \"\"\"\n",
    "        发出(异步)读取, 将用数据调用回调\n",
    "        \"\"\"\n",
    "        if not self.is_connected():\n",
    "            return\n",
    "        self._read_callback = callback\n",
    "        self._ble.gattc_read(self._conn_handle, self._value_handle)\n",
    "\n",
    "\n",
    "    def on_notify(self, callback):\n",
    "        \"\"\"\n",
    "        设置设备通知时要调用的回调\n",
    "        \"\"\"\n",
    "        self._notify_callback = callback\n",
    "\n",
    "    def _update_value(self, data):\n",
    "        \"\"\"\n",
    "        数据以摄氏度表示为sint16，分辨率为0.01摄氏度。\n",
    "        \"\"\"\n",
    "        self._value = struct.unpack(\"<h\", data)[0] / 100\n",
    "        return self._value\n",
    "\n",
    "    def value(self):\n",
    "        return self._value\n",
    "\n",
    "\n",
    "def demo():\n",
    "    ble = bluetooth.BLE()\n",
    "    central = BLETemperatureCentral(ble)\n",
    "\n",
    "    not_found = False\n",
    "\n",
    "    def on_scan(addr_type, addr, name):\n",
    "        if addr_type is not None:\n",
    "            print(\"Found sensor:\", addr_type, addr, name)\n",
    "            central.connect()\n",
    "        else:\n",
    "            nonlocal not_found\n",
    "            not_found = True\n",
    "            print(\"No sensor found.\")\n",
    "\n",
    "    central.scan(callback=on_scan)\n",
    "\n",
    "    # Wait for connection...\n",
    "    while not central.is_connected():\n",
    "        time.sleep_ms(100)\n",
    "        if not_found:\n",
    "            return\n",
    "\n",
    "    print(\"Connected\")\n",
    "\n",
    "    # 显式发出reads，使用“print”作为回调\n",
    "    while central.is_connected():\n",
    "        central.read(callback=print)\n",
    "        time.sleep_ms(2000)\n",
    "\n",
    "    # Alternative to the above, just show the most recently notified value.\n",
    "    # while central.is_connected():\n",
    "    #     print(central.value())\n",
    "    #     time.sleep_ms(2000)\n",
    "\n",
    "    print(\"Disconnected\")\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    demo()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. HC-05蓝牙主从一体模块\n",
    "- 目的: 演示AT指令\n",
    "- MicroPython的串口通讯[http://docs.micropython.org/en/latest/library/machine.UART.html](http://docs.micropython.org/en/latest/library/machine.UART.html)\n",
    "- CH-05用户说明: [./esp32/CH05蓝牙主从一体/用户手册.pdf](./esp32/CH05蓝牙主从一体/用户手册.pdf)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 资料参考：\n",
    "- [https://zhuanlan.zhihu.com/p/37717509](https://zhuanlan.zhihu.com/p/37717509)\n",
    "- [The history of the Bluetooth Special Interest Group.bluetooth.com](https://www.bluetooth.com/about-us/our-history/)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.7.3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
